/**
 * Copyright (C) 2013 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.inject.spi;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.inject.Module;
import com.google.inject.internal.util.StackTraceElements;
import com.google.inject.internal.util.StackTraceElements.InMemoryStackTraceElement;

import java.util.List;

/**
 * Associated to a {@link Module module}, provides the module class name, the parent module {@link
 * ModuleSource source}, and the call stack that ends just before the module {@link
 * Module#configure(Binder) configure(Binder)} method invocation.
 */
final class ModuleSource {

    /**
     * The class name of module that this {@link ModuleSource} associated to.
     */
    private final String moduleClassName;

    /**
     * The parent {@link ModuleSource module source}.
     */
    private final ModuleSource parent;

    /**
     * The chunk of call stack that starts from the parent module {@link Module#configure(Binder)
     * configure(Binder)} call and ends just before the module {@link Module#configure(Binder)
     * configure(Binder)} method invocation. For a module without a parent module the chunk starts
     * from the bottom of call stack. The array is non-empty if stack trace collection is on.
     */
    private final InMemoryStackTraceElement[] partialCallStack;

    /**
     * Creates a new {@link ModuleSource} with a {@literal null} parent.
     * @param module the corresponding module
     * @param partialCallStack the chunk of call stack that starts from the parent module {@link
     * Module#configure(Binder) configure(Binder)} call and ends just before the module {@link
     * Module#configure(Binder) configure(Binder)} method invocation
     */
    ModuleSource(Object module, StackTraceElement[] partialCallStack) {
        this(null, module, partialCallStack);
    }

    /**
      * Creates a new {@link ModuleSource} Object.
      * @param parent the parent module {@link ModuleSource source}
      * @param module the corresponding module
      * @param partialCallStack the chunk of call stack that starts from the parent module {@link
      * Module#configure(Binder) configure(Binder)} call and ends just before the module {@link
      * Module#configure(Binder) configure(Binder)} method invocation
      */
    private ModuleSource(
    /* @Nullable */ ModuleSource parent, Object module, StackTraceElement[] partialCallStack) {
        Preconditions.checkNotNull(module, "module cannot be null.");
        Preconditions.checkNotNull(partialCallStack, "partialCallStack cannot be null.");
        this.parent = parent;
        this.moduleClassName = module.getClass().getName();
        this.partialCallStack = StackTraceElements.convertToInMemoryStackTraceElement(partialCallStack);
    }

    /**
     * Returns the corresponding module class name.
     *
     * @see Class#getName()
     */
    String getModuleClassName() {
        return moduleClassName;
    }

    /**
     * Returns the chunk of call stack that starts from the parent module {@link
     * Module#configure(Binder) configure(Binder)} call and ends just before the module {@link
     * Module#configure(Binder) configure(Binder)} method invocation. The return array is non-empty
     * only if stack trace collection is on.
     */
    StackTraceElement[] getPartialCallStack() {
        return StackTraceElements.convertToStackTraceElement(partialCallStack);
    }

    /**
     * Returns the size of partial call stack if stack trace collection is on otherwise zero.
     */
    int getPartialCallStackSize() {
        return partialCallStack.length;
    }

    /**
     * Creates and returns a child {@link ModuleSource} corresponding to the {@link Module module}.
     * @param module the corresponding module
     * @param partialCallStack the chunk of call stack that starts from the parent module {@link
     * Module#configure(Binder) configure(Binder)} call and ends just before the module {@link
     * Module#configure(Binder) configure(Binder)} method invocation
     */
    ModuleSource createChild(Object module, StackTraceElement[] partialCallStack) {
        return new ModuleSource(this, module, partialCallStack);
    }

    /**
     * Returns the parent module {@link ModuleSource source}.
     */
    ModuleSource getParent() {
        return parent;
    }

    /**
     * Returns the class names of modules in this module source. The first element (index 0) is filled
     * by this object {@link #getModuleClassName()}. The second element is filled by the parent's
     * {@link #getModuleClassName()} and so on.
     */
    List<String> getModuleClassNames() {
        ImmutableList.Builder<String> classNames = ImmutableList.builder();
        ModuleSource current = this;
        while (current != null) {
            String className = current.moduleClassName;
            classNames.add(className);
            current = current.parent;
        }
        return classNames.build();
    }

    /**
     * Returns the size of {@link ModuleSource ModuleSources} chain (all parents) that ends at this
     * object.
     */
    int size() {
        if (parent == null) {
            return 1;
        }
        return parent.size() + 1;
    }

    /**
     * Returns the size of call stack that ends just before the module {@link Module#configure(Binder)
     * configure(Binder)} method invocation (see {@link #getStackTrace()}).
     */
    int getStackTraceSize() {
        if (parent == null) {
            return partialCallStack.length;
        }
        return parent.getStackTraceSize() + partialCallStack.length;
    }

    /**
     * Returns the full call stack that ends just before the module {@link Module#configure(Binder)
     * configure(Binder)} method invocation. The return array is non-empty if stack trace collection
     * on.
     */
    StackTraceElement[] getStackTrace() {
        int stackTraceSize = getStackTraceSize();
        StackTraceElement[] callStack = new StackTraceElement[stackTraceSize];
        int cursor = 0;
        ModuleSource current = this;
        while (current != null) {
            StackTraceElement[] chunk = StackTraceElements.convertToStackTraceElement(current.partialCallStack);
            int chunkSize = chunk.length;
            System.arraycopy(chunk, 0, callStack, cursor, chunkSize);
            current = current.parent;
            cursor = cursor + chunkSize;
        }
        return callStack;
    }
}
